package com.laviedesfons.jhmtasc.metadata;

import com.laviedesfons.jhmtasc.annotations.binding.MRootType;
import com.laviedesfons.jhmtasc.types.abstracts.AbstractType;
import com.laviedesfons.jhmtasc.types.interfaces.InternalType;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.TypeElement;
import javax.tools.FileObject;

/**
 *
 * @author Romano
 */
@SupportedAnnotationTypes(value = {"*"})
@SupportedSourceVersion(SourceVersion.RELEASE_6)
@SupportedOptions(value = {
    "AnnotationVerifier.Annotations",
    "AnnotationVerifier.Baseclasses",
    "AnnotationVerifier.ClassAnnotation.Mappings"})
public class ModelProcessor extends AbstractProcessor {

    private static final Logger logger =
            Logger.getLogger(ModelProcessor.class.getSimpleName());
    private static final String GENERATED_PACKAGE =
            ModelProcessor.class.getPackage().getName();
    private static final String TABULATION = "    ";
    private static final String idt = "";
    private static final String idt1 = idt + TABULATION;
    private static final String idt2 = idt1 + TABULATION;
    private static final String idt3 = idt2 + TABULATION;
    private static final String idt4 = idt3 + TABULATION;

    private static final Set<String> variables = new HashSet<String>();

    private static final StringBuilder typesDecl = new StringBuilder();
    private static final StringBuilder typesParams = new StringBuilder();
    private static final StringBuilder typesListAdd = new StringBuilder();
    private static final StringBuilder typesFactory = new StringBuilder();
    private static final StringBuilder typeFields = new StringBuilder();
    private static final Set<String> imports = new HashSet<String>();

    private static final Class SERVICE = TypeUtils.class;
    private static final String SERVICE_NAME = SERVICE.getSimpleName();
    private static final String SERVICE_IMPL_NAME = SERVICE_NAME+"Impl";
    private static final String SERVICE_IMPL_FULL_NAME =  GENERATED_PACKAGE + "."+SERVICE_IMPL_NAME;

    private PrintWriter createSource(String rootDirName, String name)
            throws IOException {

        FileObject fileObject;
        if (rootDirName != null && !rootDirName.isEmpty()) {
            fileObject = processingEnv.getFiler().createResource(
                    javax.tools.StandardLocation.SOURCE_OUTPUT,
                    rootDirName, name);
        } else {
            fileObject = processingEnv.getFiler().createSourceFile(name);
        }
        return new PrintWriter(fileObject.openOutputStream());
    }

    private PrintWriter createResource(String name) throws IOException {
        FileObject fileObject;
        fileObject = processingEnv.getFiler().createResource(
                javax.tools.StandardLocation.CLASS_OUTPUT,
                "",
                name);
        return new PrintWriter(fileObject.openOutputStream());
    }

    private static String genParamVar(MetaType m){
        return genTypeVar(m) + "Params";
    }

    private static String _genTypeVar(MetaType mt) {
        StringBuilder sb = new StringBuilder(mt.getSimpleName());
        if (mt.isGeneric()) {
            for (MetaType param : mt.getParams()) {
                sb.append(_genTypeVar(param));
            }
        }
        return sb.toString();
    }

    private static String genTypeVar(MetaType m) {
        String s = _genTypeVar(m);
        s = s.substring(0, 1).toLowerCase().concat(s.substring(1));
        return s + "Type";
    }

    private static String genFieldVar(MetaField f){
        StringBuilder sb = new StringBuilder();
        if(f.getContainer() != null)
            sb.append(f.getContainer().getSimpleName());
        return sb.append(f.getFieldName()).append("Field").toString();
    }

    private void genFieldSource(MetaField f, String modelVar) {

        String fieldVar = genFieldVar(f);

        // new Field (offset)
        typeFields.append(String.format(
                "%sMetaField %s = new MetaField(%s);\n",
                idt2,
                fieldVar,
                modelVar));

        // f.setFieldName("fieldName");
        typeFields.append(String.format(
                "%s%s.setFieldName(\"%s\");\n",
                idt2,
                fieldVar,
                f.getFieldName()));

        // f.setFlag("flag");
        typeFields.append(String.format(
                "%s%s.setFlag(\"%s\");\n",
                idt2,
                fieldVar,
                f.getFlag()));
        // f.setId(1);
        typeFields.append(String.format(
                "%s%s.setId(%d);\n",
                idt2,
                fieldVar,
                f.getId()));

        // f.setKind(MetaField.Kind.VALUE);
        typeFields.append(String.format(
                "%s%s.setKind(MetaField.Kind.%s);\n",
                idt2,
                fieldVar,
                f.getKind().name().toUpperCase(Locale.ENGLISH)));

        genTypeSource(f.getMetaType());

        // f.setMetaType(t);
        typeFields.append(String.format(
                "%s%s.setMetaType(%s);\n",
                idt2,
                fieldVar,
                genTypeVar(f.getMetaType())));

        // t.setParentField(f);
        if (!f.getMetaType().isPrimitive()) {
            typeFields.append(String.format(
                    "%s%s.setParentField(%s);\n",
                    idt2,
                    genTypeVar(f.getMetaType()),
                    fieldVar));
        }
    }

    private void genFieldsSource(MetaType m){
        if(!m.isPrimitive()){
            String typeVar = genTypeVar(m);
            for (MetaField f : ((MetaModel) m).getOffsetFields()) {

                genFieldSource(f, typeVar);

                // m.addOffsetField
                typeFields.append(String.format(
                        "%s%s.addOffsetField(%s);\n",
                        idt2,
                        typeVar,
                        genFieldVar(f)));

            }
            for (MetaField f : ((MetaModel) m).getValueFields()) {

                genFieldSource(f, typeVar);

                // m.addOffsetField
                typeFields.append(String.format(
                        "%s%s.addValueField(%s);\n",
                        idt2,
                        typeVar,
                        genFieldVar(f)));
            }
        }
    }

    private void genTypeDecl(MetaType m){

        imports.add(m.getName());

        String typeVar = genTypeVar(m);
        if (m.isPrimitive()) {
            // new MetaModel
            typesDecl.append(String.format(
                    "%sfinal MetaType %s = new MetaType(%s.class);\n",
                    idt2,
                    typeVar,
                    m.getSimpleName()));
        } else {
            // new MetaModel
            typesDecl.append(String.format(
                    "%sfinal MetaModel %s = new MetaModel(%s.class);\n",
                    idt2,
                    typeVar,
                    m.getSimpleName()));
        }
    }
    private void genTypeParams(MetaType m){
        String typeVar = genTypeVar(m);
        if(m.isGeneric()){
            typesParams.append(String.format(
                    "%sfinal MetaType[] %s = new MetaType[%d];\n",
                    idt2,
                    genParamVar(m),
                    m.getParams().length));

            for (int i = 0; i < m.getParams().length; i++) {
                genTypeSource(m.getParams()[i]);

                typesParams.append(String.format(
                        "%s%s[%d] = %s;\n",
                        idt2,
                        genParamVar(m),
                        i,
                        genTypeVar(m.getParams()[i])));
            }

            typesParams.append(String.format(
                    "%s%s.setParams(%s);\n",
                    idt2,
                    typeVar,
                    genParamVar(m)));
        }
    }

    private void genTypeFactory(MetaType m){
        typesFactory.append(String.format(
                "%s%s.setFactory(new Factory(){\n",
                idt2,
                genTypeVar(m)));
        typesFactory.append(String.format(
                "%s@Override\n", idt3));
        typesFactory.append(String.format(
                "%spublic InternalType create(MetaType m) {\n", idt3));

        String name = m.getSimpleName();
        String cstrParam = "";
        if (m.isPrimitive() && m.isGeneric()) {
            cstrParam = genParamVar(m);
            if (m.getParams().length == 1) {
                cstrParam += "[0]";
            }
        }

        typesFactory.append(String.format(
                "%s%s a = new %s(%s);\n",
                idt4,
                name,
                name,
                cstrParam));

        if (!m.isPrimitive()) {
            typesFactory.append(
                    String.format("%sa.setMetaModel((MetaModel)m);\n", idt4));
        }
        typesFactory.append(String.format("%sreturn a;\n", idt4));

        typesFactory.append(String.format(
                "%s}\n%s});\n", idt3, idt2));
    }

    private void genTypeSource(MetaType m) {

        if (m == null) {
            return;
        }

        String typeVar = genTypeVar(m);
        if(variables.contains(typeVar)){
            return;
        }
        variables.add(typeVar);

        genTypeDecl(m);
        genTypeParams(m);
        genFieldsSource(m);
        genTypeFactory(m);

        if(!m.isPrimitive()){
            typesListAdd.append(String.format(
                    "%s%s.setFlag(\"%s\");\n",
                    idt2,
                    typeVar,
                    ((MetaModel)m).getFlag()));

            typesListAdd.append(String.format(
                        "%saddMetaModel(%s);\n",
                        idt2,
                        typeVar));
        }

        logger.log(Level.INFO, "{0}:{1}", new Object[]{typeVar, m.toString()});
    }

    private void genServiceFile()throws IOException{
        PrintWriter printer = createResource(
                "META-INF/services/"
                + SERVICE.getCanonicalName());
        printer.println(SERVICE_IMPL_FULL_NAME);
        printer.close();
    }

    private void genServiceImpl(MetaModel m) throws IOException{

        if(m == null)
            return;

        logger.log(Level.INFO, "RootModel: {0}", m.toString());

        genTypeSource(m);
        genServiceFile();

        PrintWriter printer = createSource(null, SERVICE_IMPL_FULL_NAME);

        // package decl
        printer.append(String.format(
                "\n%spackage %s;\n\n",
                idt,
                GENERATED_PACKAGE));

        // imports
        imports.add(InternalType.class.getName());
        imports.add(MetaType.Factory.class.getName().replace('$', '.'));
        for(String imp : imports){
            printer.append(String.format(
                "%simport %s;\n",
                idt,
                imp));
        }
        printer.append(
                String.format(
                "%simport %s;\n\n",
                idt, AbstractType.class.getName()));

        // class decl
        printer.append(
                String.format(
                "%spublic class %s extends %s {\n\n",
                idt,
                SERVICE_IMPL_NAME,
                SERVICE_NAME));

        // getRootTypeClass impl
        printer.append(String.format("%s@Override\n", idt1));
        printer.append(
                String.format(
                "%sprotected Class<? extends %s> getRootTypeClass() {\n",
                idt1,
                AbstractType.class.getSimpleName()));
        printer.append(
                String.format(
                "%sreturn %s.class;\n", idt2, m.getSimpleName()));
        printer.append(String.format("%s}\n\n", idt1));

        // cstr
        printer.append(
                String.format(
                "%spublic %s() {\n\n",
                idt1,
                SERVICE_IMPL_NAME));

        printer.append(typesDecl.append("\n").toString());
        printer.append(typesParams.append("\n").toString());
        printer.append(typeFields.append("\n").toString());
        printer.append(typesFactory.append("\n").toString());
        printer.append(typesListAdd.append("\n").toString());

        printer.append(
                String.format(
                "%s%s.setIsRootType(true);\n",
                idt2,
                genTypeVar(m)));

        printer.append(String.format("%s}\n", idt1));

        // end class
        printer.append(String.format("%s}\n", idt));
        printer.close();
    }

    @Override
    public boolean process(
            Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {

        if (!roundEnv.processingOver()) {

            MetaTypeVisitor metaTypeVisitor = new MetaTypeVisitor();
            List<Element> elements = new ArrayList<Element>();
            elements.addAll(roundEnv.getElementsAnnotatedWith(MRootType.class));

            for (Element e : elements) {
                if (e.getKind() == ElementKind.CLASS) {
                    MetaModel model =
                            (MetaModel)e.asType().accept(metaTypeVisitor, null);
                    try {
                        genServiceImpl(model);
                    } catch (IOException ex) {
                        logger.log(Level.SEVERE, ex.getMessage());
                    }
                    return true;
                }
            }
            return true;
        }
        return false;
    }
}